3.09. Игры на HTML5
Разработчику
Аналитику
Тестировщику
Архитектору
Инженеру
Игры на HTML5
Игры на HTML5 — это интерактивные приложения, созданные с использованием открытых веб-стандартов и запускаемые прямо в веб-браузере, без необходимости предварительной установки дополнительного программного обеспечения.
Такие игры представляют собой естественное развитие веб-платформы от средства отображения гипертекстовых документов к полноценной среде исполнения сложных программных систем.
За последние пятнадцать лет браузер превратился в универсальную площадку, способную поддерживать высокопроизводительную анимацию, обработку пользовательского ввода в реальном времени, работу со звуком, графикой, сетевым взаимодействием и даже офлайн-функциональностью. HTML5 стал ключевым этапом в этой трансформации — не как отдельная версия языка разметки, а как собирательное обозначение современного стека веб-технологий, включающего не только HTML, но и сопутствующие спецификации: Canvas API, Web Audio API, IndexedDB, Service Workers, Pointer Lock API, Gamepad API и множество других.
В основе каждой HTML5-игры лежит трёхкомпонентная структура: HTML, CSS и JavaScript.
HTML формирует каркас документа — структуру страницы, на которой размещаются элементы интерфейса и игровое полотно.
CSS отвечает за визуальное оформление: расположение блоков, анимации, цветовые схемы, адаптивность под разные размеры экранов.
JavaScript обеспечивает динамику — логику поведения, реакцию на действия игрока, обновление состояния мира, расчёт физики, управление графикой и звуком.
Эти три технологии тесно интегрированы между собой и предоставляют разработчику полный контроль над поведением приложения в пределах окна браузера.
Особенность HTML5-игр заключается в их автономности. Для запуска игры достаточно лишь открыть один HTML-файл в браузере. В этом файле могут быть встроены стили через тег <style>, скрипты — через тег <script>, графические ресурсы — закодированными в формате Base64 и вложенными напрямую, а звуковые данные — в виде аудиофрагментов, представленных в виде массивов или Data URLs. Такой подход позволяет создавать полностью самодостаточные приложения, не зависящие от внешних серверов, сетевого подключения или дополнительных библиотек. Файл можно сохранить на диск, передать по почте, открыть на любом устройстве — игра заработает мгновенно, без установки, без регистрации, без ожидания загрузки.
Мгновенная доступность — одно из ключевых свойств HTML5-игр. Пользователь, получив ссылку, переходит по ней и видит запущенную игру через доли секунды. Нет этапов скачивания, проверки цифровой подписи, распаковки архивов, установки зависимостей. Нет необходимости выделять место на диске или беспокоиться о совместимости с операционной системой. Весь процесс сводится к одному действию: открыть страницу. Это свойство сделало HTML5-игры идеальным форматом для вирусного распространения: ссылка на игру может быть встроена в пост в социальной сети, отправлена в мессенджере, размещена в рекламном баннере, и каждый получатель получает одинаковый, немедленный доступ. Отсутствие барьеров входа напрямую повышает вовлечённость: интерес возник — и через мгновение уже можно играть.
Кроссплатформенность HTML5-игр — прямое следствие стандартизации веб-платформы. Все современные браузеры — будь то Chrome, Firefox, Safari, Edge на настольных компьютерах или Chrome для Android, Safari на iOS, Samsung Internet на мобильных устройствах — реализуют одни и те же базовые API. Это означает, что один и тот же код может работать на Windows, macOS, Linux, Android, iOS, ChromeOS без каких-либо изменений. Разработчик пишет игру один раз — и она автоматически становится доступной на миллиардах устройств по всему миру. Адаптация под сенсорные экраны, клавиатуру, мышь или геймпад осуществляется на уровне обработки событий: браузер абстрагирует физическое устройство ввода и предоставляет единый интерфейс для работы с ним. Например, событие нажатия пальца на экран (touchstart) и клика мышью (mousedown) могут обрабатываться одним и тем же обработчиком, если логика управления идентична.
Разнообразие жанров в HTML5-играх поражает своей широтой. На начальном этапе развития формата доминировали простые аркады, викторины, головоломки и логические мини-игры — приложения с низкими требованиями к производительности и графике. Со временем, по мере роста возможностей браузеров, появились более сложные проекты: платформеры с плавной анимацией, гонки с реалистичной физикой, стратегии в реальном времени, RPG с открытым миром и даже многопользовательские MMO, работающие по WebSockets. Визуальное исполнение также вышло далеко за рамки пиксель-арта: WebGL, встроенный в HTML5 через элемент <canvas>, позволяет использовать шейдеры, трёхмерные модели, освещение, тени и постобработку изображения — всё то, что раньше было доступно только в нативных приложениях. Уже сегодня можно встретить полноценные 3D-игры, запускаемые в браузере и не уступающие по качеству десктопным аналогам.
Монетизация HTML5-игр построена на гибкости и интеграции. Веб-страница — это документ, который легко модифицировать динамически. Разработчик может встроить рекламные баннеры, видеорекламу перед запуском или после проигрыша, предложения по покупке внутриигровых улучшений — и всё это средствами JavaScript и рекламных SDK, таких как Google Ad Manager, AdColony или Unity Ads. Многие игры распространяются бесплатно и получают доход за счёт показов рекламы — модель, особенно эффективная при высокой вовлечённости и вирусном распространении. Альтернативный путь — полная коммерциализация: игра упаковывается в PWA (Progressive Web App), устанавливается на устройство как обычное приложение и распространяется через веб-каталоги или собственные сайты, полностью минуя магазины приложений со своими комиссиями.
Экосистема разработки HTML5-игр включает как низкоуровневые инструменты, так и высокоуровневые фреймворки. На базовом уровне достаточно текстового редактора и браузера: программа пишется на чистом JavaScript, взаимодействует с DOM или Canvas напрямую, управляет циклом обновления вручную через requestAnimationFrame. Для упрощения работы и ускорения разработки широко применяются игровые движки и библиотеки. Phaser — один из самых популярных open-source движков, предоставляющий готовые решения для работы с графикой, анимацией, физикой, аудио, вводом и сценарием. Он поддерживает как 2D-режим (через Canvas или WebGL), так и экспериментальные 3D-возможности. PixiJS — высокопроизводительная 2D-библиотека для отрисовки, созданная с акцентом на скорость и масшттабируемость; часто используется как основа для более сложных движков. Babylon.js и Three.js — мощные 3D-фреймворки, позволяющие создавать насыщенные графические сцены в браузере. Даже Unity — традиционно нативный движок — поддерживает экспорт проектов в WebGL, что делает возможным запуск сложных игр, созданных в Unity, прямо в вебе.
Для создания контента разработчики HTML5-игр активно используют открытые ресурсы. Сайт Kenney.nl предлагает тысячи бесплатных ассетов: спрайты, тайловые наборы, 3D-модели, UI-элементы, анимации — всё под лицензией CC0 (общественное достояние). На freesound.org можно найти сотни тысяч звуковых эффектов, музыкальных фрагментов и амбиентных записей, также доступных по открытой лицензии. Эти ресурсы позволяют даже одному человеку собрать полноценную игру за считанные часы, сосредоточившись не на создании сырья, а на реализации игровой механики и баланса.
Важной частью современной HTML5-игры является её поведение в офлайн-режиме. Благодаря Service Workers и Cache API браузер может сохранять все необходимые файлы (HTML, CSS, JavaScript, изображения, звуки) локально и предоставлять доступ к игре даже при отсутствии интернета. Это свойство делает HTML5-игры особенно ценными в условиях нестабильного соединения или при использовании устройства в автономном режиме — например, в метро или в самолёте. После первого посещения игра остаётся доступной «навсегда», пока пользователь не очистит кэш вручную.
Примеры игр
Змейка
Игра «Змейка» демонстрирует классическую структуру игрового цикла — основы любой динамической программы. Цикл состоит из трёх повторяющихся этапов: ввод → обновление состояния → отрисовка.
Ввод обрабатывается через события клавиатуры: браузер регистрирует нажатия стрелок или букв WASD, и сохраняет желаемое направление движения в промежуточной переменной nextDirection. Такое разделение — текущее направление (direction) и следующее (nextDirection) — предотвращает некорректные манёвры, например, мгновенный разворот на 180 градусов, что нарушило бы правила игры.
Обновление состояния происходит с фиксированной частотой (в примере — примерно 8 кадров в секунду). На каждом шаге голова змеи перемещается на одну клетку в текущем направлении. Затем выполняются проверки: выход за границы поля, столкновение с собственным телом, совпадение координат с едой. Каждое из этих событий имеет чёткое последствие: завершение игры, увеличение счёта, генерация новой еды. Состояние змеи хранится как массив объектов {x, y}, где каждый элемент — сегмент тела. Добавление головы и удаление хвоста при отсутствии еды реализует эффект «ползания» без необходимости перестраивать всю структуру.
Отрисовка осуществляется через Canvas API — низкоуровневый интерфейс для программной генерации изображений. Каждый кадр начинается с очистки холста (clearRect), затем последовательно рисуются сегменты змеи (прямоугольники с обводкой) и еда (окружность). Простота визуализации позволяет сосредоточиться на логике, но при этом сохраняется полный контроль над пикселями: цвет головы отличается от тела, еда имеет округлую форму — всё достигается минимальным набором графических операций.
Особое внимание уделено управлению жизненным циклом: игра может быть остановлена (gameRunning = false), а экран поражения включает кнопку перезапуска, которая сбрасывает все переменные состояния к начальным значениям. Это показывает, как в HTML5-играх организуется состояние приложения — не как глобальный хаос, а как набор контролируемых переменных, управляемых через чётко определённые точки входа (инициализация, шаг, рестарт).
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Змейка</title>
<style>
body {
margin: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background: #1e1e1e;
font-family: sans-serif;
}
#gameCanvas {
border: 2px solid #444;
background: #000;
}
#score {
position: absolute;
top: 10px;
left: 10px;
color: #fff;
font-size: 18px;
}
#gameOver {
position: absolute;
display: none;
flex-direction: column;
align-items: center;
color: #fff;
font-size: 24px;
}
#restartBtn {
margin-top: 16px;
padding: 8px 16px;
font-size: 16px;
background: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
#restartBtn:hover {
background: #45a049;
}
</style>
</head>
<body>
<div id="score">Счёт: 0</div>
<canvas id="gameCanvas" width="600" height="400"></canvas>
<div id="gameOver">
<div>Игра окончена</div>
<button id="restartBtn">Начать заново</button>
</div>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const scoreEl = document.getElementById('score');
const gameOverEl = document.getElementById('gameOver');
const restartBtn = document.getElementById('restartBtn');
const gridSize = 20;
const cellCountX = canvas.width / gridSize;
const cellCountY = canvas.height / gridSize;
let snake = [{ x: 10, y: 10 }];
let food = {};
let direction = { x: 0, y: 0 };
let nextDirection = { x: 0, y: 0 };
let score = 0;
let gameRunning = true;
function placeFood() {
food = {
x: Math.floor(Math.random() * cellCountX),
y: Math.floor(Math.random() * cellCountY)
};
// Избегаем генерации еды внутри змеи
if (snake.some(segment => segment.x === food.x && segment.y === food.y)) {
placeFood();
}
}
function resetGame() {
snake = [{ x: 10, y: 10 }];
direction = { x: 0, y: 0 };
nextDirection = { x: 0, y: 0 };
score = 0;
scoreEl.textContent = `Счёт: ${score}`;
placeFood();
gameOverEl.style.display = 'none';
gameRunning = true;
}
function moveSnake() {
if (!gameRunning) return;
// Фиксируем направление только после шага (предотвращаем 180° поворот)
direction = { ...nextDirection };
if (direction.x === 0 && direction.y === 0) return;
const head = { ...snake[0] };
head.x += direction.x;
head.y += direction.y;
// Проверка выхода за границы
if (head.x < 0 || head.x >= cellCountX || head.y < 0 || head.y >= cellCountY) {
endGame();
return;
}
// Проверка столкновения с собой
if (snake.some(segment => segment.x === head.x && segment.y === head.y)) {
endGame();
return;
}
snake.unshift(head);
// Проверка поедания еды
if (head.x === food.x && head.y === food.y) {
score += 10;
scoreEl.textContent = `Счёт: ${score}`;
placeFood();
} else {
snake.pop(); // Удаляем хвост, если не съели
}
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Змея
snake.forEach((segment, index) => {
ctx.fillStyle = index === 0 ? '#4CAF50' : '#8BC34A';
ctx.fillRect(segment.x * gridSize, segment.y * gridSize, gridSize, gridSize);
ctx.strokeStyle = '#2E7D32';
ctx.strokeRect(segment.x * gridSize, segment.y * gridSize, gridSize, gridSize);
});
// Еда
ctx.fillStyle = '#FF5252';
ctx.beginPath();
ctx.arc(
(food.x + 0.5) * gridSize,
(food.y + 0.5) * gridSize,
gridSize / 2 - 2,
0,
Math.PI * 2
);
ctx.fill();
}
function gameLoop() {
moveSnake();
draw();
if (gameRunning) {
setTimeout(() => requestAnimationFrame(gameLoop), 120); // ~8.3 FPS → можно регулировать
}
}
function endGame() {
gameRunning = false;
gameOverEl.style.display = 'flex';
}
// Управление
window.addEventListener('keydown', e => {
switch (e.key) {
case 'ArrowUp': if (direction.y === 0) nextDirection = { x: 0, y: -1 }; break;
case 'ArrowDown': if (direction.y === 0) nextDirection = { x: 0, y: 1 }; break;
case 'ArrowLeft': if (direction.x === 0) nextDirection = { x: -1, y: 0 }; break;
case 'ArrowRight': if (direction.x === 0) nextDirection = { x: 1, y: 0 }; break;
case 'w': case 'W': if (direction.y === 0) nextDirection = { x: 0, y: -1 }; break;
case 's': case 'S': if (direction.y === 0) nextDirection = { x: 0, y: 1 }; break;
case 'a': case 'A': if (direction.x === 0) nextDirection = { x: -1, y: 0 }; break;
case 'd': case 'D': if (direction.x === 0) nextDirection = { x: 1, y: 0 }; break;
}
});
restartBtn.addEventListener('click', resetGame);
// Инициализация
placeFood();
gameLoop();
</script>
</body>
</html>
2048
Игра «2048» представляет собой чисто логическую систему, где визуальная составляющая служит отражением внутренней модели. Основа игры — двумерный массив 4×4, заполненный числами (степенями двойки) или нулями (пустые клетки). Весь игровой процесс сводится к преобразованию этого массива в ответ на действия игрока.
Ключевой операцией является сжатие строки с объединением равных соседей. Функция slide принимает одномерный массив, удаляет нули, последовательно объединяет пары одинаковых значений слева направо, затем дополняет результат нулями до длины четыре. Эта функция универсальна: направления вверх, вниз, влево и вправо реализуются через транспонирование, реверс и повторное применение slide. Такой подход демонстрирует мощь функционального стиля: сложные пространственные преобразования сводятся к композиции простых операций над данными.
Состояние доски обновляется только при наличии изменений — сравнение исходного и результирующего массивов предотвращает ложные ходы и сохраняет счёт чистым. После каждого успешного хода добавляется новая плитка (2 или 4) в случайную пустую ячейку. Игра завершается, когда доска заполнена и отсутствуют возможные объединения — проверка, реализованная в функции canMove, сканирует соседние ячейки на равенство.
Визуализация построена на CSS Grid и абсолютном позиционировании: фоновая сетка создаётся статически, а плитки — динамически через DOM-элементы с классами, соответствующими их значениям (.tile-2, .tile-4, …). Каждая плитка позиционируется с помощью процентных отступов, что обеспечивает адаптивность. Цвета и размеры шрифтов изменяются в зависимости от значения — это пример декларативного стиля: внешний вид определяется данными, а не процедурным кодом.
Хранение рекорда через localStorage показывает, как HTML5-игры могут сохранять долгосрочные данные без сервера. Это минимальная, но полноценная реализация сохранения прогресса — один из важнейших аспектов пользовательского опыта.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>2048</title>
<style>
* { box-sizing: border-box; }
body {
font-family: Arial, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
margin: 0;
background: #faf8ef;
color: #776e65;
}
#header {
text-align: center;
margin-bottom: 20px;
}
#score, #best {
font-size: 20px;
font-weight: bold;
}
#grid {
position: relative;
width: 400px;
height: 400px;
background: #bbada0;
border-radius: 6px;
padding: 16px;
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-gap: 16px;
}
.cell {
width: 100%;
aspect-ratio: 1 / 1;
background: rgba(238, 228, 218, 0.35);
border-radius: 6px;
}
.tile {
position: absolute;
width: calc(25% - 20px);
height: calc(25% - 20px);
border-radius: 6px;
display: flex;
justify-content: center;
align-items: center;
font-weight: bold;
font-size: 32px;
transition: all 0.1s ease;
}
/* Цвета плиток — по оригиналу */
.tile-2 { background: #eee4da; color: #776e65; }
.tile-4 { background: #ede0c8; color: #776e65; }
.tile-8 { background: #f2b179; color: #f9f6f2; }
.tile-16 { background: #f59563; color: #f9f6f2; }
.tile-32 { background: #f67c5f; color: #f9f6f2; }
.tile-64 { background: #f65e3b; color: #f9f6f2; }
.tile-128 { background: #edcf72; color: #f9f6f2; font-size: 28px; }
.tile-256 { background: #edcc61; color: #f9f6f2; font-size: 28px; }
.tile-512 { background: #edc850; color: #f9f6f2; font-size: 28px; }
.tile-1024 { background: #edc53f; color: #f9f6f2; font-size: 24px; }
.tile-2048 { background: #edc22e; color: #f9f6f2; font-size: 24px; }
#message {
margin-top: 20px;
font-size: 24px;
font-weight: bold;
height: 30px;
}
#restart {
margin-top: 10px;
padding: 8px 16px;
font-size: 16px;
background: #8f7a66;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>
</head>
<body>
<div id="header">
<h1>2048</h1>
<div>
<span id="score">Счёт: 0</span> |
<span id="best">Рекорд: 0</span>
</div>
</div>
<div id="grid"></div>
<div id="message"></div>
<button id="restart">Новая игра</button>
<script>
const gridEl = document.getElementById('grid');
const scoreEl = document.getElementById('score');
const bestEl = document.getElementById('best');
const messageEl = document.getElementById('message');
const restartBtn = document.getElementById('restart');
let board = Array(4).fill().map(() => Array(4).fill(0));
let score = 0;
let best = localStorage.getItem('2048-best') || 0;
bestEl.textContent = `Рекорд: ${best}`;
// Инициализация сетки визуальных ячеек (фон)
for (let i = 0; i < 16; i++) {
const cell = document.createElement('div');
cell.className = 'cell';
gridEl.appendChild(cell);
}
function addTile(value, row, col) {
const tile = document.createElement('div');
tile.className = `tile tile-${value}`;
tile.textContent = value;
tile.style.top = `${row * 25 + 4}%`;
tile.style.left = `${col * 25 + 4}%`;
tile.dataset.row = row;
tile.dataset.col = col;
gridEl.appendChild(tile);
}
function clearTiles() {
const tiles = gridEl.querySelectorAll('.tile');
tiles.forEach(t => t.remove());
}
function render() {
clearTiles();
for (let r = 0; r < 4; r++) {
for (let c = 0; c < 4; c++) {
if (board[r][c] !== 0) {
addTile(board[r][c], r, c);
}
}
}
scoreEl.textContent = `Счёт: ${score}`;
}
function addRandomTile() {
const empty = [];
for (let r = 0; r < 4; r++) {
for (let c = 0; c < 4; c++) {
if (board[r][c] === 0) empty.push([r, c]);
}
}
if (empty.length === 0) return false;
const [r, c] = empty[Math.floor(Math.random() * empty.length)];
board[r][c] = Math.random() < 0.9 ? 2 : 4;
return true;
}
// Сжатие строки влево (без учёта границ)
function slide(row) {
const filtered = row.filter(v => v !== 0);
const merged = [];
let i = 0;
while (i < filtered.length) {
if (i + 1 < filtered.length && filtered[i] === filtered[i + 1]) {
const newVal = filtered[i] * 2;
merged.push(newVal);
score += newVal;
i += 2;
} else {
merged.push(filtered[i]);
i += 1;
}
}
while (merged.length < 4) merged.push(0);
return merged;
}
function moveLeft() {
let moved = false;
for (let r = 0; r < 4; r++) {
const original = [...board[r]];
board[r] = slide(board[r]);
if (!arraysEqual(original, board[r])) moved = true;
}
return moved;
}
function moveRight() {
let moved = false;
for (let r = 0; r < 4; r++) {
const original = [...board[r]];
board[r] = slide([...board[r]].reverse()).reverse();
if (!arraysEqual(original, board[r])) moved = true;
}
return moved;
}
function moveUp() {
let moved = false;
for (let c = 0; c < 4; c++) {
const col = [board[0][c], board[1][c], board[2][c], board[3][c]];
const original = [...col];
const newCol = slide(col);
for (let r = 0; r < 4; r++) board[r][c] = newCol[r];
if (!arraysEqual(original, newCol)) moved = true;
}
return moved;
}
function moveDown() {
let moved = false;
for (let c = 0; c < 4; c++) {
const col = [board[0][c], board[1][c], board[2][c], board[3][c]];
const original = [...col];
const newCol = slide([...col].reverse()).reverse();
for (let r = 0; r < 4; r++) board[r][c] = newCol[r];
if (!arraysEqual(original, newCol)) moved = true;
}
return moved;
}
function arraysEqual(a, b) {
return a.length === b.length && a.every((v, i) => v === b[i]);
}
function canMove() {
// Есть пустые ячейки?
for (let r = 0; r < 4; r++)
for (let c = 0; c < 4; c++)
if (board[r][c] === 0) return true;
// Есть соседи для слияния?
for (let r = 0; r < 4; r++) {
for (let c = 0; c < 4; c++) {
const v = board[r][c];
if (
(r > 0 && board[r - 1][c] === v) ||
(r < 3 && board[r + 1][c] === v) ||
(c > 0 && board[r][c - 1] === v) ||
(c < 3 && board[r][c + 1] === v)
) return true;
}
}
return false;
}
function gameOver() {
messageEl.textContent = 'Игра окончена';
if (score > best) {
best = score;
localStorage.setItem('2048-best', best);
bestEl.textContent = `Рекорд: ${best}`;
}
}
function winCheck() {
for (let r = 0; r < 4; r++)
for (let c = 0; c < 4; c++)
if (board[r][c] === 2048) {
messageEl.textContent = 'Победа!';
return true;
}
return false;
}
function handleMove(moved) {
if (!moved) return;
if (addRandomTile()) {
render();
if (!canMove()) gameOver();
else winCheck();
} else {
gameOver();
}
}
function resetGame() {
board = Array(4).fill().map(() => Array(4).fill(0));
score = 0;
messageEl.textContent = '';
addRandomTile();
addRandomTile();
render();
}
window.addEventListener('keydown', e => {
if (messageEl.textContent) return; // игнорируем ввод после конца
let moved = false;
switch (e.key) {
case 'ArrowLeft': moved = moveLeft(); break;
case 'ArrowRight': moved = moveRight(); break;
case 'ArrowUp': moved = moveUp(); break;
case 'ArrowDown': moved = moveDown(); break;
case 'a': case 'A': moved = moveLeft(); break;
case 'd': case 'D': moved = moveRight(); break;
case 'w': case 'W': moved = moveUp(); break;
case 's': case 'S': moved = moveDown(); break;
}
if (moved) handleMove(true);
});
restartBtn.addEventListener('click', resetGame);
// Запуск
resetGame();
</script>
</body>
</html>
Три в ряд
Игра «Три в ряд» вводит концепцию каскадных изменений — ситуации, когда одно действие вызывает цепную реакцию последующих событий. Игрок меняет местами две соседние плитки. Если в результате образуется линия из трёх или более одинаковых символов, они удаляются, плитки выше падают вниз, новые появляются сверху, и процесс повторяется, пока совпадения существуют.
Эта логика требует разделения на этапы: поиск совпадений → визуальное исчезновение → обновление доски → падение → генерация новых плиток → рекурсивная проверка. Каждый этап занимает время и должен быть визуально заметен игроку — иначе игра будет ощущаться как «скачущая» и непредсказуемая. Поэтому используется асинхронная обработка с задержками: setTimeout и Promise обеспечивают паузы между фазами, позволяя браузеру отрисовать промежуточные состояния (например, исчезновение плиток через прозрачность .removing).
Состояние доски представлено как двумерный массив строковых символов — эмодзи. Это не просто декор: эмодзи работают везде, не требуют загрузки шрифтов или изображений, и обеспечивают мгновенную визуальную идентификацию. Проверка валидности начальной доски (hasValidMove) гарантирует, что игра начнётся с возможного хода — важный момент в проектировании игрового баланса.
Управление взаимодействием включает защиту от конфликтов: флаг isProcessing блокирует ввод во время анимации каскада, предотвращая «наложение» действий. Выделение выбранной плитки через класс .selected даёт визуальную обратную связь. Обработка как клавиш WASD, так и стрелок, обеспечивает комфорт на разных устройствах — ещё одна деталь, повышающая доступность.
Таким образом, даже в миниатюрной реализации «Три в ряд» заложены основы сложных систем: обработка событий, управление состоянием, анимационные последовательности, рекурсивные алгоритмы и защита от некорректного ввода.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Три в ряд</title>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
background: #222;
color: white;
margin: 0;
padding: 20px;
}
#info {
margin-bottom: 16px;
font-size: 20px;
}
#grid {
display: grid;
grid-template-columns: repeat(8, 50px);
grid-gap: 2px;
background: #333;
padding: 6px;
border-radius: 4px;
}
.cell {
width: 50px;
height: 50px;
display: flex;
justify-content: center;
align-items: center;
font-size: 28px;
cursor: pointer;
user-select: none;
border-radius: 4px;
transition: transform 0.2s, opacity 0.2s;
}
.cell.selected {
transform: scale(0.9);
box-shadow: 0 0 8px #ff0 inset;
}
.cell.removing {
opacity: 0;
}
#message {
margin-top: 16px;
font-size: 20px;
min-height: 26px;
}
</style>
</head>
<body>
<div id="info">Счёт: <span id="score">0</span> | Ходы: <span id="moves">30</span></div>
<div id="grid"></div>
<div id="message"></div>
<script>
const ROWS = 8, COLS = 8;
const MAX_MOVES = 30;
const TILES = ['🍇', '🍎', '🍊', '🍋', '🍒', '🍑']; // 6 типов
let board = [];
let score = 0;
let moves = MAX_MOVES;
let selected = null;
let isProcessing = false;
const gridEl = document.getElementById('grid');
const scoreEl = document.getElementById('score');
const movesEl = document.getElementById('moves');
const messageEl = document.getElementById('message');
// Инициализация доски
function initBoard() {
board = Array(ROWS).fill().map(() => Array(COLS).fill(null));
do {
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
board[r][c] = TILES[Math.floor(Math.random() * TILES.length)];
}
}
} while (!hasValidMove()); // перегенерировать, если нет ходов
render();
}
// Отрисовка
function render() {
gridEl.innerHTML = '';
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
const cell = document.createElement('div');
cell.className = 'cell';
cell.textContent = board[r][c];
cell.dataset.r = r;
cell.dataset.c = c;
cell.addEventListener('click', () => handleCellClick(r, c));
gridEl.appendChild(cell);
}
}
}
function updateUI() {
scoreEl.textContent = score;
movesEl.textContent = moves;
}
// Проверка валидности хода (есть ли хотя бы одна комбинация ≥3)
function hasValidMove() {
// Горизонталь
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS - 2; c++) {
const a = board[r][c], b = board[r][c+1], d = board[r][c+2];
if (a === b || b === d || a === d) return true;
}
}
// Вертикаль
for (let c = 0; c < COLS; c++) {
for (let r = 0; r < ROWS - 2; r++) {
const a = board[r][c], b = board[r+1][c], d = board[r+2][c];
if (a === b || b === d || a === d) return true;
}
}
return false;
}
function handleCellClick(r, c) {
if (isProcessing || moves <= 0) return;
const cellEl = document.querySelector(`[data-r="${r}"][data-c="${c}"]`);
if (!selected) {
selected = { r, c };
cellEl.classList.add('selected');
return;
}
const { r: r1, c: c1 } = selected;
const dr = Math.abs(r - r1), dc = Math.abs(c - c1);
const isAdjacent = (dr === 1 && dc === 0) || (dr === 0 && dc === 1);
if (!isAdjacent) {
// сброс выбора
document.querySelector('.selected')?.classList.remove('selected');
selected = { r, c };
cellEl.classList.add('selected');
return;
}
// Обмен
[board[r1][c1], board[r][c]] = [board[r][c], board[r1][c1]];
render(); // мгновенное отображение обмена
const matches = findMatches();
if (matches.length === 0) {
// откат
[board[r1][c1], board[r][c]] = [board[r][c], board[r1][c1]];
render();
document.querySelector('.selected')?.classList.remove('selected');
selected = null;
return;
}
// Применяем ход
moves--;
document.querySelector('.selected')?.classList.remove('selected');
selected = null;
updateUI();
// Запускаем каскадное удаление
setTimeout(() => processMatches(matches), 300);
}
function findMatches() {
const matches = new Set();
// Горизонтальные
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS - 2; c++) {
const a = board[r][c], b = board[r][c+1], d = board[r][c+2];
if (a && a === b && b === d) {
matches.add(`${r},${c}`);
matches.add(`${r},${c+1}`);
matches.add(`${r},${c+2}`);
}
}
}
// Вертикальные
for (let c = 0; c < COLS; c++) {
for (let r = 0; r < ROWS - 2; r++) {
const a = board[r][c], b = board[r+1][c], d = board[r+2][c];
if (a && a === b && b === d) {
matches.add(`${r},${c}`);
matches.add(`${r+1},${c}`);
matches.add(`${r+2},${c}`);
}
}
}
return Array.from(matches).map(pos => {
const [r, c] = pos.split(',').map(Number);
return { r, c };
});
}
async function processMatches(matches) {
if (matches.length === 0) {
if (moves <= 0) {
messageEl.textContent = 'Игра окончена';
}
return;
}
isProcessing = true;
// Визуальное исчезновение
matches.forEach(({ r, c }) => {
const el = document.querySelector(`[data-r="${r}"][data-c="${c}"]`);
if (el) el.classList.add('removing');
});
await new Promise(r => setTimeout(r, 300));
// Удаление из доски
matches.forEach(({ r, c }) => {
board[r][c] = null;
score += 10;
});
// Падение
for (let c = 0; c < COLS; c++) {
const column = [];
for (let r = ROWS - 1; r >= 0; r--) {
if (board[r][c] !== null) column.push(board[r][c]);
}
while (column.length < ROWS) column.push(TILES[Math.floor(Math.random() * TILES.length)]);
for (let r = 0; r < ROWS; r++) {
board[ROWS - 1 - r][c] = column[r];
}
}
render();
updateUI();
// Рекурсивная проверка каскада
const nextMatches = findMatches();
setTimeout(() => {
isProcessing = false;
processMatches(nextMatches);
}, 200);
}
// Запуск
initBoard();
updateUI();
</script>
</body>
</html>
Игровые движки
Когда проект выходит за рамки простой мини-игры, ручное управление каждым пикселем, каждым событием и каждым кадром становится неэффективным. Ошибки в синхронизации, дублирование кода, сложность отладки и рост технического долга требуют применения более зрелых решений. Игровые движки для HTML5 — это готовые программные каркасы, предоставляющие разработчику стандартизированные интерфейсы для работы с графикой, физикой, аудио, сценой, состоянием и вводом. Они не заменяют JavaScript, а организуют его использование в соответствии с проверенными архитектурными паттернами.
Phaser
Phaser — один из самых зрелых и широко используемых open-source движков для создания 2D-игр в браузере. Его философия — предоставить максимальную функциональность «из коробки», оставаясь при этом гибким и расширяемым. Движок состоит из трёх основных слоёв: ядро, сцена и компоненты.
Ядро (Phaser.Game) отвечает за инициализацию браузерного окружения: выбор режима рендеринга (Canvas или WebGL), создание игрового цикла на основе requestAnimationFrame, управление жизненным циклом сцен, обработку событий потери/возврата фокуса, работу с устройствами ввода. Оно создаётся один раз при запуске и служит центральной точкой координации.
Сцена (Phaser.Scene) — это логический и визуальный контейнер, объединяющий всё, что происходит в определённом состоянии игры: меню, уровень, экран победы. Каждая сцена имеет собственный жизненный цикл: preload (загрузка ресурсов), create (инициализация объектов), update (игровая логика на каждом кадре). Сцены могут существовать параллельно (например, основная игра и интерфейс HUD), переключаться мгновенно или с переходами — это даёт гибкость в архитектуре.
Компоненты — строительные блоки игровых объектов. Sprite — растровое изображение с позицией, масштабом, поворотом и анимациями. Text — стилизованный текст с шрифтами, тенями, выравниванием. Graphics — векторная графика для отрисовки линий, фигур, градиентов. Tilemap — мощная система для работы с тайловыми картами: загрузка Tiled-файлов, слои, свойства тайлов, коллизии. Physics — модульная система физики: Arcade (лёгкая, для аркад), Matter.js (реалистичная, с многоугольниками, шарнирами, трением) или Ninja (специализированная для платформеров). Sound — единый интерфейс для работы с Web Audio API и HTML5 Audio, включая фоновую музыку, звуковые эффекты, групповую громкость и пространственное позиционирование.
Особая сила Phaser — в его системе состояний и событий. Любой объект может генерировать и слушать события: pointerdown, animationcomplete, collide, scene.start. Это позволяет строить слабо связанные компоненты, где, например, кнопка не знает о существовании игрового мира, но при нажатии посылает событие menu:start-level, на которое реагирует менеджер сцен. Такой подход повышает тестируемость и повторное использование кода.
Phaser поддерживает сборку через Webpack, Rollup или Vite, что позволяет использовать современные JavaScript-возможности (модули, TypeScript, JSX-подобные шаблоны), оптимизировать и минифицировать код, встраивать ресурсы. Он имеет огромное сообщество, тысячи примеров, плагины для аналитики, монетизации, локализации — и при этом остаётся легковесным (около 500 КБ сжатого кода).
PixiJS
Если Phaser — это «автомобиль», то PixiJS — это «двигатель». Это не полноценный игровой движок, а специализированная 2D-рендеринговая библиотека, построенная поверх WebGL с fallback’ом на Canvas. Её цель — максимально быстро отрисовывать графику, масштабировать спрайты, применять фильтры и анимировать объекты. PixiJS часто используется как база для собственных движков или в составе более крупных фреймворков (в том числе — в ранних версиях Phaser).
Архитектура PixiJS основана на сценографическом дереве (display list). Корнем дерева является Application, который создаёт холст и запускает цикл рендеринга. Все визуальные элементы — Sprite, Container, Graphics, Text — являются наследниками DisplayObject и могут быть вложены друг в друга. Контейнеры (Container) позволяют группировать объекты и трансформировать их как единое целое: перемещение контейнера смещает всех потомков, поворот вращает всю группу. Это особенно удобно для UI (панели с кнопками), интерфейсов (инвентарь с предметами) или сложных анимаций (персонаж с частями тела).
Производительность PixiJS достигается за счёт пакетной обработки вызовов отрисовки. Вместо отдельного drawImage для каждого спрайта библиотека собирает все однотипные операции и выполняет их одним вызовом GPU — это снижает нагрузку на CPU и повышает частоту кадров. Поддержка атласов (TexturePacker) позволяет загружать десятки или сотни спрайтов из одного изображения, минимизируя количество HTTP-запросов и ускоряя рендеринг.
PixiJS не включает физику, управление сценами или систему событий высокого уровня — это сознательный выбор. Разработчик сам решает, какую физическую библиотеку подключить (например, planck.js или matter.js), как организовать управление состоянием (через конечные автоматы или Redux-подобные хранилища), как обрабатывать ввод (pixi.js-extensions для pointer/touch/gamepad). Такой модульный подход даёт контроль и предсказуемость, но требует больше усилий на начальном этапе.
Оба инструмента — Phaser и PixiJS — совместимы с современными практиками веб-разработки. Они поддерживают TypeScript, работают в средах с модульной системой, интегрируются с инструментами сборки и тестирования. Выбор между ними определяется масштабом проекта: для прототипа, обучения или казуальной игры — Phaser; для высокооптимизированного приложения, где каждый кадр на счету, или для построения собственного движка — PixiJS.
Процесс разработки
Разработка HTML5-игры — это последовательность этапов, каждый из которых решает определённую задачу и требует специфических инструментов. В отличие от нативных приложений, где сборка и развёртывание часто связаны с компиляцией и подписанием, HTML5-проекты остаются по своей сути набором текстовых файлов. Тем не менее, профессиональный подход предполагает систематизацию, автоматизацию и контроль качества на всех стадиях.
Проектирование и прототипирование
Первый шаг — определение механики, правил и пользовательского потока. На этом этапе важна скорость проверки гипотез: идея должна быть реализована в работающем виде за часы, а не недели. Для этого подходит «голый» HTML+CSS+JS, как в примерах выше, или визуальные инструменты вроде Tweak.style, CodePen, JSFiddle — они позволяют мгновенно видеть результат, делиться ссылкой с коллегами, собирать обратную связь. Прототип не обязан быть красивым: квадраты вместо спрайтов, console.log вместо анимаций — допустимы, если они подтверждают работоспособность основной идеи.
Ключевой документ — игровая спецификация: описание целей, правил, состояний (меню, игра, пауза, победа, поражение), входных действий (какие клавиши/жесты), выходных реакций (что происходит при нажатии). Для сложных проектов добавляется карта сцен, схема состояний (state machine), мокапы интерфейса. Важно зафиксировать баланс: сколько ходов даётся в «Три в ряд», как растёт скорость в «Змейке», какие комбинации приносят сколько очков в «2048». Без этих параметров игра может оказаться слишком лёгкой или непроходимой.
Сборка и модульность
Когда прототип подтверждает жизнеспособность идеи, наступает этап структурирования кода. Единый HTML-файл уступает место модульному проекту:
index.html— точка входа, минимальный шаблон с подключением стилей и скриптов.src/— исходный код:game.js— основной игровой цикл и управление состояниями,scenes/— отдельные файлы для меню, уровня, финального экрана,entities/— классы игровых объектов (змейка, плитка, враг),systems/— логические модули (физика, генерация, сохранение),assets/— графика, звуки, шрифты.
public/— статические ресурсы для развёртывания.build/— результат сборки.
Для управления зависимостями и сборкой применяются современные инструменты: Vite, Webpack, Parcel. Vite, например, обеспечивает мгновенную перезагрузку при изменении кода (HMR), поддержку ES-модулей без транспиляции, встроенную оптимизацию изображений и шрифтов. Сборка преобразует исходники в один или несколько минифицированных JavaScript-файлов, объединяет CSS, хеширует имена ресурсов для долгосрочного кэширования.
Особое внимание — загрузке ресурсов. Игра не должна начинаться, пока не загружены все спрайты, звуки и шрифты. Используются менеджеры загрузки (в Phaser — this.load), которые отслеживают прогресс и показывают экран загрузки. Для критических ресурсов применяется предварительная загрузка (<link rel="preload">), для некритичных — ленивая загрузка по мере необходимости.
Тестирование и отладка
Тестирование HTML5-игр охватывает несколько уровней:
-
Функциональное тестирование — проверка логики:
- змейка не проходит сквозь себя,
- плитки в 2048 сливаются только по правилам,
- в «Три в ряд» нет ложных срабатываний при обмене.
Для этого пишутся unit-тесты с помощью Jest или Vitest, где вызываются функцииmoveLeft,findMatchesс разными входными данными и проверяется ожидаемый вывод.
-
Интерфейсное тестирование — взаимодействие пользователя:
- клик по кнопке запускает игру,
- пауза останавливает цикл,
- управление работает на клавиатуре, сенсоре и геймпаде.
Здесь применяются инструменты вроде Cypress или Playwright, имитирующие действия пользователя и проверяющие состояние DOM или Canvas.
-
Кросс-браузерное и кросс-платформенное тестирование — запуск на разных устройствах:
- Chrome, Firefox, Safari, Edge на десктопе,
- Android (Chrome, Samsung Internet), iOS (Safari),
- старые версии браузеров (если требуется поддержка).
Используются сервисы BrowserStack, LambdaTest, Sauce Labs, или локальные виртуальные машины. Особое внимание — поведению на мобильных: масштабирование, ориентация, сенсорные зоны, энергопотребление.
-
Производительное тестирование — замер частоты кадров, времени загрузки, потребления памяти.
Встроенные инструменты браузера (Chrome DevTools → Performance, Memory) позволяют записать профиль выполнения, найти «тяжёлые» функции, утечки памяти, избыточные перерисовки. Цель — стабильные 60 FPS на целевых устройствах (включая бюджетные смартфоны), загрузка за 3 секунды на 3G.
Оптимизация
Оптимизация HTML5-игр — это баланс между качеством и производительностью. Основные направления:
-
Графика:
— Использование атласов (sprite sheets) вместо отдельных изображений.
— Выбор формата: WebP для фотографий и сложных текстур, PNG для прозрачных спрайтов с чёткими краями, SVG для иконок и UI.
— Сжатие без потерь черезoptipng,pngquant,svgo.
— Размеры кратны степени двойки (256×256, 512×512) для эффективной работы GPU. -
Аудио:
— Конвертация в Opus (для WebM) или AAC (для MP4), как наиболее поддерживаемые и эффективные форматы.
— Разделение на короткие эффекты (WAV/OGG) и длинную музыку (MP3/AAC).
— Предзагрузка и кэширование черезAudioContext.decodeAudioData. -
Код:
— Минификация и сжатие (gzip/brotli) на стороне сервера.
— Устранение мёртвого кода (tree-shaking) через сборщики.
— Кэширование через HTTP-заголовки (Cache-Control: max-age=31536000для хешированных ресурсов).
— ИспользованиеrequestIdleCallbackдля неважных задач (аналитика, предзагрузка следующего уровня). -
Canvas:
— Очистка только изменённых областей (clearRectс координатами, а не всего холста).
— Отключение сглаживания (imageSmoothingEnabled = false) для пиксель-арта.
— Повторное использование объектов вместо создания новых в каждом кадре (object pooling).
Публикация и распространение
Готовая игра может быть опубликована множеством способов:
-
Как веб-страница — самый простой путь. Файлы размещаются на хостинге (GitHub Pages, Netlify, Vercel), и игра доступна по URL. Для улучшения вовлечённости добавляется мета-информация: Open Graph (картинка, описание для соцсетей), Twitter Cards,
manifest.jsonдля PWA. -
Как Progressive Web App (PWA) — игра получает иконку на домашнем экране, работает офлайн, ведёт себя как нативное приложение. Для этого требуется:
—manifest.jsonс именем, иконками, темой, режимом отображения (standalone),
— Service Worker, кэширующий ресурсы,
— HTTPS (обязательно для PWA).
После установки игра запускается без адресной строки, имеет собственный процесс, может отправлять уведомления. -
В каталогах HTML5-игр — таких как CrazyGames, GamePix, itch.io, Kongregate. Они предоставляют аудиторию, монетизацию через рекламу, аналитику. Интеграция обычно требует добавления SDK (например,
CrazyGames.SDK) для согласования пауз, показа баннеров, отслеживания событий. -
Как встраиваемый iframe — игра публикуется на стороннем сайте (блог, портал, образовательная платформа). Для безопасности используется
sandbox-атрибут, для коммуникации —postMessageAPI. -
Как экспорт из Unity/Construct/GDevelop — если игра создана в визуальном редакторе, финальный шаг — экспорт в WebGL, который затем может быть дополнительно оптимизирован и упакован.
Важно предусмотреть аналитику: сколько запусков, среднее время сессии, точка отсева (на каком уровне игроки уходят), конверсия в монетизацию. Простые решения — Google Analytics 4 с событиями game_start, level_complete, ad_shown; более гибкие — собственные endpoint’ы или open-source системы вроде Plausible или Matomo.